4

I'd like to shorten one of my scripts and I got na idea but I dont know how to do that. Have a piece of code like this:

COMMAND="find /etc -type "
case $1:
 "directory") $COMMAND d
 ;;
esac

Of course this is the short version :) And now I want to be able to shorten it, not towrite that $COMMAND everywhere, so I would like to have something like this:

$COMMAND <the case statement return value>

but I dont want to use a variable to store the result of case.

Thanks :) Hope you understood what I want :D

EDIT 1: Its possible to create a function and pass the find parameter as $1 as pointed by Serg. Now, IF I wanted to do it just without the function, im sure there's a way :D Not like Serg didnt solve it, I'm just curious :D

  • 2
    Turn it into a function and pass type as $1 argument, then issue call to function in case statements. I am on mobile so cannot post well formated answer , so maybe someome else can ir i will do it in about an hour or si – Sergiy Kolodyazhnyy Nov 12 '15 at 2:48
  • Thats a nice idea. It solves my problem, thank you :) – IcyIcyIce Nov 12 '15 at 2:51
  • 1
    Posted an answer. Please mark it as accepted if you found that it solves your problem. Thank you in advance and ask more questions on the site ! :) – Sergiy Kolodyazhnyy Nov 12 '15 at 3:55
  • Adding an edit to simplify the answer. The function approach is standard, but in your specific question, this will work – Sergiy Kolodyazhnyy Nov 12 '15 at 4:07
  • 1
    @underscore_d that's a good solution that shortens the code. I'd suggest you post this as an answer. – Sergiy Kolodyazhnyy Nov 12 '15 at 8:12
7

There are several possible ways to simplify the code. Below are the solutions ordered by the amount of code:

  1. printf and parameter substitution (no error checking)
  2. xargs and parameter substitution (no error checking)
  3. find and parameter substition only (no error checking)
  4. fall-through case structure (Solves error checking issue )
  5. Test logic, xargs, and parameter substitution
  6. Bash function
  7. Array, for-loop, test, and &&

  1. printf and parameter substitution solution

    A little known quality of the printf function is that if you call printf "%c" someString, it will print only the first character of that string. Thus, we can avoid using case statement with parameter expansion and printf like so:

    xieerqi:$ cat someScript.sh
    #!/bin/bash
    find /etc -type $(printf "%c" $1 )
    

    now execute:

    xieerqi:$ sudo ./someScript.sh  directory | head
    [sudo] password for xieerqi: 
    /etc
    /etc/logrotate.d
    /etc/apm
    /etc/apm/event.d
    /etc/apm/scripts.d
    /etc/apm/resume.d
    /etc/apm/suspend.d
    /etc/speech-dispatcher
    /etc/speech-dispatcher/modules
    /etc/speech-dispatcher/clients
    

    A limitation here is that we're forking a process to call printf, which the function solution avoids - function and case structure are all native bash tools.

  2. xargs and parameter substitution

    Using bash's parameter substitution we can chop off a substring from a variable (for example ${VAR:0:3} gives first 3 characters of VAR); in this case we want the first character for a type directory or file. Then we can use xargs to pass that as parameter to find

    echo ${1:0:1} | xargs -I {} find /etc -type  {} 
    

    The find man page mentions that for -type flag on Solaris there is something known as a door file, which is represented by the capital letter D, but since we're on Linux, we can say this is a limitation that is reasonable to ignore.

    However, there is another danger in this code - if a user enters flower as the $1 parameter, this is still going to search for -type f, because we take first char of any string … In other words, there's no error checking.

  3. find with parameter expansion

    Taking parameter expansion even further, we can do this:

     find /etc -type ${1:0:1}
    

    Basically , a one-liner with find command and a substring of the $1 variable. Also, no error-checking.

  4. Fall-through case structure

    The big issue with the last three methods is the error checking. They are good when you trust the user not to be a dummy, or just writing code for yourself. Now, in Java it's possible to write a switch statement that will run the same command for multiple cases if you just omit the break command. In bash that can be done as well with ;& terminator. Quote from man bash

    Using ;& in place of ;; causes execution to continue with the list associated with the next set of patterns.

    All we have to do is test for the types, like "directory", "file", "block" and so on, and then use parameter substitution to chop off the first character. Like so

    #!/bin/bash
    case "$1" in
      "directory") ;&
      "file");&
      "block") find /etc -type  ${1:0:1} ;;
      *) exit 1 ;;
    esac
    

5.Test logic, xargs, and parameter substitution

 Basic idea here is that we're sending $1 variable through pipe to `xargs`, which in turn substitutes it into test (again square brackets). `xargs` in turn builds the actual command that runs by replacing `{}` with whatever was passed to `xargs`.  As for test command, it's simple or statement,  `EXPRESSION -o EXPRESSION ` , where we test if string $1 is equal to either "file" or "directory string"

    echo "$1" | xargs -I {}  [ "{}" = "file" -o "{}" = "directory"  ] \
    && find /etc -type ${1:0:1}

 `xargs` is really useful when you need to process multiple argumens with the same command. Considering that in this case we only have one argument that needs to be processed with the same command , this can be simplified to

    [ "$1" = "file" -o "$1" = "directory"  ] && find /etc -type ${1:0:1} 

Of course the big limitation is that if you test for more than one type, you need longer `[ EXPR1 -o EXPR2 ]` structure with multiple `-o` parts.
  1. Function solution

    find command can be placed into a function, which then can be called with positional parameters.

    For instance:

    function findStuff
    {
     find /etc -type "$1" 
    }
    

    Here's a small demo. Notice I am using sudo because for a lot of files in /etc regular users don't have read permissions

    xieerqi:$ sudo ./someScript.sh directory | head                                            
    [sudo] password for xieerqi: 
    /etc
    /etc/logrotate.d
    /etc/apm
    /etc/apm/event.d
    /etc/apm/scripts.d
    /etc/apm/resume.d
    /etc/apm/suspend.d
    /etc/speech-dispatcher
    /etc/speech-dispatcher/modules
    /etc/speech-dispatcher/clients
    
    xieerqi:$ sudo ./someScript.sh file | head                                                 
    [sudo] password for xieerqi: 
    /etc/hosts.deny
    /etc/logrotate.d/speech-dispatcher
    /etc/logrotate.d/pm-utils
    /etc/logrotate.d/rsyslog
    /etc/logrotate.d/yate
    /etc/logrotate.d/apport
    /etc/logrotate.d/apt
    /etc/logrotate.d/consolekit
    /etc/logrotate.d/fail2ban
    /etc/logrotate.d/cups-daemon
    
    xieerqi:$ cat someScript.sh                                                                
    #!/bin/bash
    function findStuff
    {
     find /etc -type "$1" 
    }
    
    case "$1" in 
      "directory")findStuff d ;;
      "file") findStuff f;;
    esac
    
  2. Array, for-loop, test, and &&

    Basic idea here - match user's input against a list, and if matched do something. We create an array of items to check for, have a test condition (square brackets by the way are alias to test command), and just run a loop to test the $1 variable. && operator allows executing command if and only if what's on the left of && was successful. So if we found a string that's in the array, we execute find command. The ${1:0:1} was discussed in previous examples - parameter expansion that chops - off the first character from our matched type. So this solution has error checking and whole-lot of code packed into just 3 lines (4 if you include #! line).

    #!/bin/bash   
    array=("file" "directory" "block");
    for TYPE in "${array[@]}"; do 
       [ "$1" = "$TYPE"  ] && find /etc/ -type ${1:0:1}; 
    done  
    
| improve this answer | |
  • +1 for being so comprehensive and including a lot of additional concerns way beyond what was asked (but very important) – underscore_d Nov 12 '15 at 8:06
4

Put it into a function:

MyFind () {
  COMMAND="find /etc -type "
  case $1:
   "directory") $COMMAND d
   ;;
  esac
}

Now you can always use it as MyFind $TYPE


Regarding your 1st comment

You can also put only the case statement into a function

FType () {
  case $1 in
    "directory") echo d;;
    "file") echo f;;
  esac
}

COMMAND="find /etc -type "
$COMMAND $(FType $TYPE) 
| improve this answer | |
  • I know I can go like case: "something") $Command d something command l something command s but Id like to avoid writing command all the time . Is there a way to do it like: $COMMAND {the case statement} ? – IcyIcyIce Nov 12 '15 at 2:54
2
[[ "$1" == "directory" ]] && find /etc -type d
| improve this answer | |
  • Yes, a possible solution, hence upvoted, but that means OP has to write multiple lines like this to check multiple types. For example to check for all types listed in find man page, that would be 8 lines code. This may work for simple scripts, but for complex scripts may be redundant and case ...esac structure would be preferred, besides it does the same job. Main point though is that OP wanted to write find command one time, and just give letter as argument to sort of "standard" find command, in other words simplify the process. Nonethelss, I like your answer – Sergiy Kolodyazhnyy Nov 12 '15 at 6:59
  • Side note: there's no need to enclose variables containing whitespaces in double quotes in [[]] (and strings without whitespaces regardless): [[ $1 == directory ]] [...]. – kos Nov 12 '15 at 7:07
2

Another approach:

declare -A foo    # associative array

foo["directory"]="d"
foo["file"]="f"
foo["link"]="l"

find /etc -type ${foo["$1"]}
| improve this answer | |
1

Just another idea to add to the already wide set of options from Serg's excellent answer.

You might be able to use an alias for this - possibly with some slight changes to how you currently do things.

An alias is simply a word that you choose to map to a larger string, which the shell will expand whenever it encounters it. Perhaps the most commonly seen use case is to apply 'defaults' to an existing command, like this:

alias ls='ls -a --color=auto'

However, there's no requirement that your alias has to be named after an existing command - or even shorter than its destination pattern - so you could e.g. alias tgzcreator='tar -czvf'

aliases share lifetime with the shell in which they are defined. 'Persistent' aliases for you can be set up in ~/.bash_aliases, which should be sourced automatically by most well-written default .bashrc scripts and suchlike.

Note a couple of hints:

  • Rather than worrying in a particular script whether a previously defined alias will interfere with your code, you can prefix it with a backslash to ensure aliasing is skipped. e.g. I normally alias cp to cp -i to avoid accidentally overwriting things, but in some scripts, I clearly want to overwrite things. (I'm not going to use some awful workaround like setting up a known unaliased user!) So, in that script, I'll use \cp src dst
  • aliases may not be sourced by default within shell scripts, which launch their own non-interactive copy of the shell. You can ensure they are expanded by setting the option expand_aliases in your script. I got this from: https://stackoverflow.com/questions/2197461/how-to-set-an-alias-inside-a-bash-shell-script-so-that-is-it-visible-from-the-ou

So, basing off the limited context available in your post, you might want to do something kinda like this:

shopt -s expand_aliases
alias get_etc_dir='find /etc -type d'
alias get_etc_fil='find /etc -type f'

For you, this may not work without tweaking, e.g. changing your parameterised inode type into a per-alias suffix. It's just another option for how users can shorten bits of code in general. Either way, I've tried to explain comprehensively based on what I know, and I hope it's useful somewhere.

(Also, I'd suggest moving this to Unix/Linux SE, assuming that's the ideal for things that aren't Ubuntu-specific?)

| improve this answer | |
  • Very nice. I would simplify this even further : alias expands to a string, just like assigning it to a variable. So doing alias findStuff='find /etc/ -type and then calling it with findStuff d would work. Not sure if that would work in a script though, as I've never used aliases in scripts, mostly functions – Sergiy Kolodyazhnyy Nov 12 '15 at 8:50
  • True. When I said it didn't natively support the same parameterisation as the original $COMMAND, I meant the conditional contraction of the directory parameter into just d. That should probably be done using just one of your methods, rather than adding alias into the mix too! But yes, of course, if you just want the literal parameter, simply type it after. – underscore_d Nov 12 '15 at 10:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.