6 ms·
I love bash and I know I'm not alone. Sure, it's syntax is based on ideas from the 1960s[0] and it has a weird core library of functions[1]. Certainly writing
by fooblat 7y ago
I love bash and I know I'm not alone.
Sure, it's syntax is based on ideas from the 1960s[0] and it has a weird core library of functions[1]. Certainly writing code that is a blend of logic and managing other programs takes some getting used to. However, it is very well documented[2] and I've had great success with it over the past 20 years.
If one takes the time to get to know it, it is actually easy and fun to write scripts that are robust and easy to maintain.
I'd venture to say that bash is the putty in the little gaps of the internet. It is the fitting that glues programs to the various systems on which they run.
0. https://en.wikipedia.org/wiki/ALGOL https://en.wikipedia.org/wiki/ALGOL
1. https://en.wikipedia.org/wiki/POSIX https://en.wikipedia.org/wiki/POSIX
2. https://www.gnu.org/software/bash/manual/ https://www.gnu.org/software/bash/manual/
edit: formatting
- bordercases 7y agoIt's also extremely fast.
- kazinator 7y agoSure, until you need to do something advanced like have a function return a string and use it in the caller. Test case: #!/bin/bash fun() { echo "result" } RES=$(fun) echo $RES Snippet from system call trace (via "strace bash ./test.sh"): pipe([3, 4]) = 0 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 rt_sigprocmask(SIG_BLOCK, [INT CHLD], [], 8) = 0 lseek(255, -10, SEEK_CUR) = 51 clone(child_stack=0, flags=CLONE_CHILD_CLEARTID|CLONE_CHILD_SETTID|SIGCHLD, child_tidptr=0x7fccbed72a10) = 3870 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 rt_sigaction(SIGCHLD, {0x446240, [], SA_RESTORER|SA_RESTART, 0x7fccbe3b0ff0}, {0x446240, [], SA_RESTORER|SA_RESTART, 0x7fccbe3b0ff0}, 8) = 0 close(4) = 0 read(3, "result\n", 128) = 7 read(3, "", 128) = 0 In other words, this requires a pipe to be created a child process to be spawned, and its output to be read from that pipe. In any reasonable scripting language, and even some unreasonable ones, this is all done in one address space. How about, oh, Awk: function fun() { return "result" } One of the most common mantras for effective shell programming is "avoid writing loops; orchestrate external utilities".
- Spivak 7y agoThe way one typically accomplishes this in bash is the following since the $() construct spawns a subshell. #!/bin/bash fun() { res="result" } fun && echo "$res" The same strace snippet. openat(AT_FDCWD, "./test.sh", O_RDONLY) = 3 stat("./test.sh", {st_mode=S_IFREG|0755, st_size=53, ...}) = 0 ioctl(3, TCGETS, 0x7ffd65c6d890) = -1 ENOTTY (Inappropriate ioctl for device) lseek(3, 0, SEEK_CUR) = 0 read(3, "#!/bin/bash\n\nfun() {\n res=\"resu"..., 80) = 53 lseek(3, 0, SEEK_SET) = 0 prlimit64(0, RLIMIT_NOFILE, NULL, {rlim_cur=1024, rlim_max=512*1024}) = 0 fcntl(255, F_GETFD) = -1 EBADF (Bad file descriptor) dup2(3, 255) = 255 close(3) = 0 fcntl(255, F_SETFD, FD_CLOEXEC) = 0 fcntl(255, F_GETFL) = 0x8000 (flags O_RDONLY|O_LARGEFILE) fstat(255, {st_mode=S_IFREG|0755, st_size=53, ...}) = 0 lseek(255, 0, SEEK_CUR) = 0 read(255, "#!/bin/bash\n\nfun() {\n res=\"resu"..., 53) = 53 fstat(1, {st_mode=S_IFCHR|0620, st_rdev=makedev(0x88, 0x2), ...}) = 0 write(1, "result\n", 7result ) = 7 read(255, "", 53) = 0 rt_sigprocmask(SIG_BLOCK, [CHLD], [], 8) = 0 rt_sigprocmask(SIG_SETMASK, [], NULL, 8) = 0 exit_group(0) = ? If you want use the old magics to return values like the built-ins then it's a little uglier but a lot cleaner. #!/bin/bash fun() { eval $1=\""result"\" } fun res && echo $res
- kazinator 7y agoAssignment to hard-coded global variable is not a return mechanism, and generally a nonstarter. It's not a viable approach for generally returning strings out shell functions everywhere in a codebase as a matter of habit. Use of eval: still slow, because the shell eval re-processes input from the character level and up. eval should be generally avoided as much as possible in shell programming. Careless use of eval can introduce security holes (piece of untrusted datum gets evaled as an expression). You really need to have your black belt in "shell escaping karate". Producing output and capturing with command substitution is the primary idiom for getting text out of a shell function. It has no visible side effect. The rebinding of standard output is scoped to the process substitution (and is confined to the child process, in fact), and the creation of the temporary process and pipe, expensive as they might be, are invisible to the program semantics. > The way one typically accomplishes this in bash In summary, what you propose here is not only vanishingly atypical, but also bad coding practice.
- aidenn0 7y agoI find it to be quite slow; I wrote a command-line arguments parser library using shell metaprogramming, and I had to precompile the output for bash, while in dash it was fast enough to run as-is.
- LukeShu 7y agoI suspect that the slowness was caused by Bash having a couple of pathological cases where it converts back and forth between the LANG/LC encoding and internal wide-characters repeatedly, which can cause it to be stupidly slow. I suspect that setting LC_ALL=C for that portion of the code would speed Bash up to Dash speeds.
- aidenn0 7y agoFactor of 3 speedup, thanks!
- deleted 7y ago[deleted]
- 3xblah 7y agoThe article mentions loops with an incrementing variable, e.g., i=$((i+1)). It is also possible to do this without loops. Is it faster. Left to the reader to decide. I used this technique sometimes so I could check on the progress of a script while it is running and also continue a script later where I left off. It also allowed me to stop the script by removing a file. At the time, the shell I used had not yet implemented LINENO, which is also very useful when scripts are terminated before finishing. For example, if I wanted to "loop" 15 times^1 To begin, first create a file that stores the count.^2 It also acts as a way to stop the "loop" from advancing if it is removed. echo 0 > x Then create a file that acts as an "on/off switch" to stop the script, e.g., if it is not runnning in the foreground. > x-on Then run the script. The "prologue" and "epilogue" use only shell built-ins. No external programs or Bash-isms are required.^3 #! /bin/sh CRAWL_DELAY=30; test -f x||exit $LINENO; test -f x-on||exit $LINENO; read x < x; test $x -le 14||exit $LINENO; # do stuff; echo https://example.com/$x; sleep CRAWL_DELAY; echo $((x+1)) >x; test -f x-on||exit $LINENO; test -f $0||exit $LINENO; $0 To see progress, cat x To stop the script before it finishes, rm x-on To restart the script and continue where left off, > x-on and run the script. 1. Sometimes I did not have access to a program like seq or jot so something like this was not possible for x in $(seq 15);do echo https://example.com/$x; done 2. I always work in a tmpfs-mounted directory so these "files" are just memory, they are not saved on "disk". 3. Bash has too many features for this author to keep track of, like, e.g., base conversion echo $((16#a))