Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP - Bash code review #209

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
Draft

Conversation

ingydotnet
Copy link
Contributor

Per #208 I wish to code review the Bash content and make suggestions on possible improvements.

I haven't done this before so I'm not sure of the best way to go about it.
I'm thinking to use this pull request, where I will make a commit for each suggestion with a couple of code changes to demonstrate the objective.
Then we can can discuss it.

I don't expect the PR to ever be merged, but you might end up cherry-picking some commits in it.

Let me know if this approach works for you and I'll start adding commits soon after.

@okurz
Copy link
Member

okurz commented Jan 27, 2023

The change itself is fine. Feel welcome to continue adding more in this PR or create an individual PR right away. I don't mind trivial PRs :)

@ingydotnet
Copy link
Contributor Author

Started out today making a bunch of changes to the Makefile.
Tried to make them into many separate commits, which is tricky when you get on a refactoring roll.
I spend nearly as much time working on Makefiles as I do with Bash. :)

To be clear, this code review will likely happen in fits and spurts over the next several (7-10?) days, as I have time and ambition.

I'll get into the Bash files tomorrow...

@ingydotnet
Copy link
Contributor Author

Just pushed a bunch of Bash review commits. Kept almost all changes to just _common as it had a lot to offer.

I still have lots more to get to, but out of time for today.

Makefile Outdated Show resolved Hide resolved
@okurz
Copy link
Member

okurz commented Jan 30, 2023

about "Add a run-with Makefile function" I am not sure it's a good idea to "just" warn. I would prefer a fatal failure.

_common Outdated
Comment on lines 70 to 74
[[ "$rc" != 0 ]] && warn "curl ($(caller)): Error fetching ($*): $response" && return 1
[[ "$rc" == 0 ]] || die "curl ($(caller)): Error fetching ($*): $response"
status_code=$(echo "$response" | tail -1)
[[ "$status_code" != 200 ]] && warn "curl ($(caller)): Error fetching url ($*): Got Status $status_code" && return 1
[[ "$status_code" == 200 ]] || die "curl ($(caller)): Error fetching url ($*): Got Status $status_code"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but calling die is having a different effect than just returning from this function. I have not checked all calls of runcurl but if you want to replace the usage here then we should make sure it's effectively having the same effect

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a very good point.
exit is not same as return, though with set -e returning non-zero could trigger an exit depending on how it was called.

This inspires me to create a test coverage facility.

_common Outdated
[[ "$rc" == 0 ]] && echo "$output" && return
[[ $rc -eq 0 ]] && echo "$output" && return
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I honestly knew about the subtleties of == vs. eq in bash but still prefered == as a more readable variant. You wrote what we should use but if there isn't a more compelling reason I'd prefer to just keep the "=="

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I suppose if shellcheck isn't griping then it's very likely ok.
I hadn't thought about using == for numbers before but I can't yet think of a reason it wouldn't work.
In that case I'd stick with it. I'll let you know if I find any edge cases.
I might start using this as well if it's safe. :)

@mergify
Copy link
Contributor

mergify bot commented Jan 30, 2023

This pull request is now in conflicts. Could you fix it? 🙏

@okurz
Copy link
Member

okurz commented Jan 30, 2023

everyone, #211 was merged but I highly recommend everyone to read all the git commit messages including the ones of already merged commits as @ingydotnet's explanations are really good and helpful for the future as well :)

@ingydotnet
Copy link
Contributor Author

about "Add a run-with Makefile function" I am not sure it's a good idea to "just" warn. I would prefer a fatal failure.

See 31022d7

aka - Wrap some long lines in README.md

This is just a benign first commit so that I can create a pull request
for this repository.

Per os-autoinst#208 I wish to code
review the Bash content and make suggestions on possible improvements.

I haven't done this before so I'm not sure of the best way to go about
it.
I'm thinking to use this pull request, where I will make a commit for
each suggestion with a couple of code changes to demonstrate the
objective.
Then we can can discuss it.

I don't expect the PR to be merged, but you might end up cherry-picking
some commits in it.

Let me know if this approach works for you and I'll start adding commits
soon after.
The run-with function checks to make sure that the runner command is
installed before running the test.

If not installed it issues a warning message.
It used to just fail, which causes all of `make test` to fail.

Also before there where lines to check if shellcheck and yamllint were
installed. They printed a msg but did not stop the commands from
attempting to run only to obviously fail for not being installed.

There was no previous check for py.test which was always failing for me
since I didn't have it installed.

Now when something isn't installed that test is just skipped with an
explanation message.
When I write any one-off bash that doesn't use a framework like BPAN,
the first line I write is a `die` function, because its always and often
needed.

I've defined the most basic one here. Since warn is now multi-line, die
is as well (if you ever need that).

The `bpan` CLI defines a minimal one right away here:
https://github.com/bpan-org/bpan/blob/main/bin/bpan#L5

Until it can bootstrap to load a full featured one from here:
https://github.com/bpan-org/bashplus/blob/main/lib/bashplus/err.bash
Personally I try to keep all of my coding in any language or format
under 80 columns, but that's just a personal preference.

However, it has allowed me to learn a lot of tricks for splitting lines
in Bash.

In this commit I show a bunch of them. And hopefully you agree then
changes make things more readable.

The best thing to know is that you can split a line after a `|`, `&&` or
`||` token. Bash knows you aren't finished with that command and so no
backslash `\` is required to continue.

Next, `$(...)` forms a code block within. You can put any Bash inside
and it can be multiline. The inner Bash follows all the same rules as
the outer Bash.

The long jq coomands are long strings and bash supports strings to be
multiline. Since jq doesn't mind ws to have newlines, it works the same.
Note I put `|` chars in jq in front rather than at end of lines, because
these `|` have no special meaning to Bash here.

I defined header in 2 commands instead of one, as an example for
splitting a long line with a cleaner look.

`$'...'` is not just a way to quote a newline in Bash. It's a full
quoting syntax like `"..."` or `'...'`. It works like `'...'` but
supports escapes (but not interpolation).
Try `echo $'foo\nbar'`.
I think it's great that you use `[[` instead of `[`.

The former is keyword syntax while that latter acts as a command.
Being syntax allows Bash to be able to treat what's inside specially.

The best thing is you almost never need to use quotes inside `[[`.

You do need to double quote a variable on the RHS for the `==` and `!=`
operators but shellcheck wil catch that.

Speaking of spellcheck is great about catching things that are a problem
but not good at pointing out unrequired syntax. I don't like quoting
things that don't need quotes as it makes people wonder why you quoted
them in the first place. Quoting implies a special semantic intent.

You should use the == != <= etc for string comparison and eq ne le etc
for numbers. Opposite of Perl. Bourne shell came first so good on Perl
for switching it!

Note: I left in `== 200` because I think of http status codes as
numberic strings. I'm honestly not sure how to treat them. Still
learning :)

The `-n` operator is never needed. `[[ $foo ]]` means same as `[[ -n
$foo ]]` and `[[ ! $foo ]]` means same as `[[ -z $foo ]]`.

Changed `[[ $dry_run == 1 ]]` to `[[ $dry_run ]]` as I don't think `1`
is special here.
If a function has no desirable side-effects (changing directory, setting
global vars) you can define them like:

    fn() ( ... )

instead of

    fn() ( ...; )

and that means the same as

    fn() { ( ... ); }

The nice thing about this is you don't ever need to use `local` because
the subprocess doesn't allow you to change global state.

Note that a one line function declared this way also needs no final
semicolon.

I changed all your functions that didn't have side-effects to use this
form.

I try to use this form unless I need to set state. It ends up leading to
better overall programming decisions.

Note that functions called in a tight loop that need to be fast as
possible should not use this form as subprocesses incur a slight
overhead. Generally this should not be a concern.
I'm pretty sure this is a clean refactoring of that function.
I thought this logic reads a bit clearer, but not that important.
The echo and cat commands are rarely needed to set up stdin for a
process.

Here I use <<< redirection which is cleaner (and likely faster).

As you can see, in Bash redirections can be placed anywhere in a
command, not just at the end.

These all work the same:

    foo a b c <i >o
    <i foo a b c >o
    >o <i foo a b c
    foo a <o b >i c
The 'make test' command will now error when dependencies are not
installed.

For py.test I made it use a virtualenv install of py.test and requests
modules. The Makefile looks for a Python 3 binary and errors otherwise.

I had python3 installed locally but not py.test. I didn't feel like that
should stop me from installing it temporarily on the fly like we do for
bpan.
I put all my best practice setup lines in _common.

From conversations with @perlpunk I think that the environments your
Bash code runs in would all have Bash 4.2+.
Bash 4.4 or higher would be something to aim for.

Since _common is a Bash library and not an executable the shebang line
is not needed, however it's probably there as a syntax highlighter hint
since there is no `.bash` extension on this file.
A better name might be `_common.bash` or `.common.bash` or
`lib/common.bash`.

I changed a couple scripts to use `#!/usr/bin/env bash` shebang lines.
This is my preference for all my open source Bash that needs to run
everywhere.
One cool aspect of this is on OSX where `/bin/bash` must be v3.2 you can
do:

    (shopt -s compat44) || die "Bash 5.0+ required. Try 'brew install bash'."

Since brew will install to `/usr/local/bin` and that always comes before
`/bin` in a standard/sane `PATH`, Mac users can use Bash 5 with scripts
without changing /bin/bash.

I realize that OSX is not a target env for you, but you could do the
same trick to require a newer Bash on Suse Linux which I understand is
still at 4.2.

Finally by source-ing _common first thing in all your scripts you can
skip the `set -e ...` code as _common does it for you.
It turns out that 'make test-shellcheck' was not picking up the test/
files. The `grep` command I added picks up everything the other command
did, plus the test/ files.

After that shellcheck griped about a lot in the test files.
I've fixed the problematic ones and silenced the others.
@ingydotnet
Copy link
Contributor Author

Rebased master and added some more commits.

The following assignment forms do not need the RHS quoted:

    a=word
    b=$var
    c=$(cmd)
    d=word-$var-$(cmd --even="with $args")

I personally strive not to use unnecessary quotes as it implies they
are needed for some purpose.

Note: shellcheck doesn't care about this since it's valid albeit useless
syntax.

Also note that `${foo-}` is idiomatic for `${foo:-''}`. You see it often
in things like:

    [[ ${SOME_VAR-} ]] && do-something

where SOME_VAR might be undefined.
@ingydotnet
Copy link
Contributor Author

Ironically 983e105 is what I imagined would be my first review commit as it is a pet peeve.

So much cargo-culted bash scripts and stackoverflow posts over-quote...

While '.' is a nice alias of 'source' to use at the command line, it's
better to stick to 'source' in Bash code. Just my opinion, but it's more
clear to future readers (and easier to grep for).

I remove extra 'set -e' calls for things sourcing _common since it is
handled in there.

I don't really like putting flags like '-e' on the shebang line.
Shebangs are not very portable across platforms.

Use '${BASH_SOURCE[0]}' to get file path rather than '$0'. '$0' is set
to 'bash' if the file is sourced rather than ran. It's useful in testing
to have an executable script also be source-able. You can unit test the
functions that way.

My standard way to call a 'main' function is:

    [[ $0 != "${BASH_SOURCE[0]}" ]] || main "$@"

Note using == w/ && here doesn't work the same. Don't do that.

Final note: http://redsymbol.net/articles/unofficial-bash-strict-mode/
makes a terrible suggestion of setting the 'IFS' variable. imho, it
shows a misunderstanding of what IFS provides.
See how it affects "$*" by running:

    (IFS=$'\t\n'; set -- a b c; echo "$*")

I just noticed they do give an alternative at the bottom of that page.
The "alternative" is actually the rudimentary usage of double quoting
"$@" which you should always do. Shellcheck will flag if you don't.

PS Setting IFS locally (per command) is very useful for controlling
specific operations, but be careful to never change it globally.
@mergify
Copy link
Contributor

mergify bot commented Feb 21, 2023

This pull request is now in conflicts. Could you fix it? 🙏

3 similar comments
@mergify
Copy link
Contributor

mergify bot commented May 2, 2023

This pull request is now in conflicts. Could you fix it? 🙏

@mergify
Copy link
Contributor

mergify bot commented Sep 26, 2023

This pull request is now in conflicts. Could you fix it? 🙏

Copy link
Contributor

mergify bot commented Jan 18, 2024

This pull request is now in conflicts. Could you fix it? 🙏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants