šŸ“° DRY(Do not Repeat Yourself) in fish

Posted on April 21, 2022 by Myoungjin Jeon
Tags: fish, shell, script, function, util, DRY

Fish Shell Series

  1. Good, So So, Bad about Fish Shell
  2. DRY(Do not Repeat Yourself) in fish

When We Need Functions

Moving files, compling source codes, checking out git repositories ā€¦ To get jobs done, We are frequently use command line tool.

Even if just change your working diretory ā€“ behind the scene ā€“ your shell is working for you to get informative prompt thesedays, isnā€™t it?

To do some work easier or faster, we could recall by searching your shell history or you could make a function to do the tasks.

What is Function in Fish Shell

A piece of codes

A function is a block of code that is reusable and performs certain operations.1 And Iā€™d like to remind you an UNIX principle, KISS!

KISS: Keep it simple and stupid

We need to best of ourselves but keep our programme simple and stupid at the same time, to make a function:

WARNING: It is different from the code block.

A code block inherit parent local variables:

fish> set -l local_test "outside"
fish> begin # begin a code block
        echo $local_test
     end
"outside"

However, function donā€™t inherit them by default.

fish> set -l local_test "outside"
fish> function echo_local_test # begin a code block
        echo $local_test
     end
fish> echo_local_test

fish>

But you can read with -S option. Iā€™d like to talk about little later.

A part of your session

Function is so common in progamming langauges, however, I found that it has some different aspects along with the similarity.

Even though We could call them in a shell, A function is not an individual programme.

fish> function sleep_10sec; sleep 10; end
fish> sleep_10sec &
... paused ...
^C
fish: Job 1, 'echo_e &' has ended
fish> # note I canceled by pressing Ctrl-C on x86_64 linux

So if you want to function act as a programme, you need to put in a separate file. and execute in another (child) fish shell. But how to make a shell script is beyond this article, Iā€™ll post about it maybe another article.

function as aliasing

I use emacs daily, and sometimes I use emacs in terminal as well and below function will make an alias for shorter name of the programme.

# ~/.config/fish/functions/em.fish
function em --description 'alias em emacs -nw $argv'
    emacs -nw $argv
end

function mc --description 'alias mc emacsclient $argv'
    emacsclient -tc $argv
end

Another famous example of aliasing might be:

function rm
    rm -i $argv
end

and fish shell provide wrapper function alias as well.

alias rmi="rm -i"

This is actually care about more like below:

alias rmi="rm -i"

# This is equivalent to entering the following function:
function rmi --wraps rm --description 'alias rmi=rm -i'
    rm -i $argv
end

ā€“wraps rm provides autocompletion which is same as rm. and makes a description on behalf of you.

how about abbr?

works differently, but also helpful as abbr replace your typing words and you can still edit your typing as well. Please read about this for more information.

Function requirement

Variable

To use variables in function is common, even though we donā€™t need at all sometimes. a variable could be set by set. One big diffent thing from bash is that set doesnā€™t require any = sign. So you could possibly make some typo

<<test target>>

fish> set -l local_var = "my_example_value"
fish> set -S local_var
$local_var: set in local scope, unexported, with 2 elements
$local_var[1]: |=|
$local_var[2]: |my_example_value|

so local_var above becomes an array. Which makes me hard to debug sometimes. because fish will trust you. As there was no syntax error.

Return value

A function is not a programme, but at the same time return value is quite similar to a programme as return value will always be an unsigned char(integer) value.

fish> function test_return; return -1; end
fish> test_return
fish> echo $status
255
fish> functions -e test_return

There are some way to save its return value, however using echo and using command subsitution is a common way because it is common for unix tiny programmes, to communicate each other via pipe, fifo.

fish> set today (date "+%Y-%m-%d")
fish> echo $today
2022-04-20

Arguments

As you can see in rmi alias in the prior example, $argv is a special variable which takes all the arguments you passed.

fish> function print_first_arg; echo $argv[1]; end
fish> print_first_arg "hi" "fish" "shell"
"hi"

Note: Index is string from 12

Input / Output

I/O is communication. Nn the communication between Your shell and function or function to another function, we will use shell subsitution like the prior example. we can use pipe |. And this is how KISS works, too.

fish>  echo "test.org" | sed 's/\.org$/\.md/'
test.md

And those kind of I/O action quite important and used very often in shell programming.

Function named ā€˜functionā€™

Now it is time to make a function. fish has straight function. meanwhile go lang has func, kotlin has fun, rust has fn ā€¦

-d option

this is an optional but quite helpful when you decide to make a function. Do you remember ā€˜KISSā€™? To clarify what you excatly want to get from the function is the main key. some lines of description will do the basic guide line.

function elem -d 'determine first argument occurs in the list(rest of arguments)'
    # do the job
end

-S option

fish shell has distinguishable concepts in variable scope. To to access local variables in the parent, We need to turn on the -S option.

-S or ā€“no-scope-shadowing

allows the function to access the variables of calling functions. Normally, any variables inside the function that have the same name as variables from the calling function are ā€œshadowedā€ ā€¦

So it is possible to some function check parent local variable, too.

function elem -d 'determine first argument occurs in the list(rest of arguments)' \
         -S
if set -q given_list # note not 'set -q $given_list'
    # use given_list variable to test
else
    # or reading from the rest of arguments
end

It depends on your function design, but in this case, we can say that local variable is safer to use here.

Note: bash doesnā€™t have local variable outside a function.

bash> local a="Apple"
bash: local: can only be used in a function
bash>

Refactor our example ā€˜elemā€™ function

Now, letā€™s focus on elem function which was imcomplete.

  1. elem function will determine first argument occurs in the list(rest of arguments). so we need some loop to go through our argument.

  2. if the first argument occurs again elem function will return true or return false (more specifically, return ā€˜return value of trueā€™, return ā€˜return value of falseā€™)

  3. and If possible when we found first arument in the rest of aruments, it would be nicer we can quit earlier.

our first elem function

function elem -d 'determine first argument occurs in the list(rest of arguments)'
    set found 0
    for arg in $argv[2..-1]
        if test $found -eq 0 && test $arg = $argv[1]
            set found 1
            break
        end
    end

    if test $found -eq 1
        true
    else
        false
    end
end

put into functions directory for permanent access

In bash, A function could be save in your ~/.bashrc or equivalent file. On the other hand, fish provides more organized way to save it. In other words, you could put a function into your ~/.config/fish/functions/ ! Genrally the filename is same as your function name. In the prior case, you could name it ~/.config/fish/functions/elem.fish. and now you can use them in another fish shell section or next login as well!

still need to source

But if you start from put a function in the functions directory, you still need to source sometimes as your change may be not yet reloaded.

fish> source ~/.config/fish/functions/elem.fish

Wait.. why I made ā€˜elmā€™ function?

I used this function when I wanto add some path to my executable $PATH, And actually, This whole article is starting from this my post on dev.to. so.. I have created a recursion in my blog posts.

Footnotes


  1. Introduction to Function in Shell Scripting: https://www.educba.com/function-in-shell-scripting/ā†©ļøŽ

  2. Index starting from 1 not 0 : https://jeongoon.github.io/posts/2022-04-16-about-fish-shell.html#index-starting-from-1-not-0ā†©ļøŽ