0

Goal

I would like to install multiple APT packages using a file.

apt_install_from_file "packages" # assumes 'packages' is a readable file in the $PWD

Note: I understand that there are other ways of doing this and I am re-inventing the wheel to speak, but I am hoping this way could make others lives easier.

File-Structure

packages:

# ppa
ppa "deadsnakes/ppa"

# packages
apt "tree"

# deb
deb "fzy" [args: "https://github.com/jhawthorn/fzy/releases/download/0.9/fzy_0.9-1_amd64.deb", "$HOME", "fzy_0.9-1_amd64.deb"]

Requirements

  1. Ignore any line that starts with # because that is a comment
  2. If the line starts with ppa then run the following command with the text enclosed by the quotes. e.g. ppa "deadsnakes/ppa" => sudo add-apt-repository -y ppa:deadsnakes/ppa &> /dev/null
  3. If the line starts with apt then run the following command with the text enclosed by the quotes. e.g. apt "tree" => sudo apt install --allow-unauthenticated -qqy tree
  4. If the line starts with deb then grab the text enclosed by quotes as well as grab the text that follows args enclosed by quotes.

Current Function:

apt_install_from_file() {

    declare -r FILE_PATH="$1"

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Install package(s)

    if [ -e "$FILE_PATH" ]; then

        cat < "$FILE_PATH" | while read -r LINE; do
            if [[ "$LINE" == *"#"* || -z "$LINE" ]]; then
                continue
            elif [[ "$LINE" == *"ppa"* || -z "$LINE" ]]; then
                # ppa= Grab text enclosed by quotes following "ppa"
                sudo add-apt-repository -y ppa:"$ppa" &> /dev/null \
                   && sudo apt update
            elif [[ "$LINE" == *"apt"* || -z "$LINE" ]]; then
                package="$LINE" # Grab text enclosed by quotes following "apt"
                sudo apt install --allow-unauthenticated -qqy "$package"
            elif [[ "$LINE" == *"deb"* || -z "$LINE" ]]; then
                #package_name= Grab text enclosed by quotes following "deb"
                #url= Grab text enclosed by quotes following "args"
                #target_path= Grab text enclosed by quotes following "args"
                #file_name= Grab text enclosed by quotes following "args"
                #file_path="$target_path" + "/" + "$file_name"  

                wget $url -O $file_path && \
                   sudo dpkg -i $file_path && sudo apt-get install -f && \
                   sudo rm -rf $file_path && sudo apt autoremove -qqy
            fi
        done

    fi

}

Problem

I need help filling in the blanks to the function given my requirement list.

  • 2
    It would be much faster to fill an array and call apt etc. just once on it. Instead of using sudo in the script, run the whole thing as root. You can install .deb files with apt -i in one go, no need to resolve depency problems this way. – dessert Jul 8 '18 at 14:01
  • 2
    Is your input file's structure carved in stone or can you change it? – PerlDuck Jul 8 '18 at 14:03
  • @PerlDuck If there is a better more organized way to do this, then I am open to it. – Nicholas Adamou Jul 8 '18 at 14:05
  • @NicholasAdamou for starters, it would be a lot simpler if you nuked most of those quotes, and changed the array into simple space-separated words. – muru Jul 8 '18 at 15:40
  • 1
    @NicholasAdamou I mean deb fzy https://github.com/jhawthorn/fzy/releases/download/0.9/fzy_0.9-1_amd64.deb $HOME fzy_0.9-1_amd64.deb – muru Jul 8 '18 at 16:03
1

The grammar being used here isn't doing shell scripting any favours, but it will be slightly easier to parse that file if you use regular expressions. Bash's regular expression comparison saves groups (...) in the BASH_REMATCH array, so you can, in one go, test for a valid line and parse it. For example:

#! /bin/bash
declare -A regex
regex["comment"]='^#(.*)'
regex["ppa"]='ppa "(.*)"'
regex["apt"]='apt "(.*)"'
regex["deb"]='deb "(.*)" \[args: "(.*)", "(.*)", "(.*)"\]'
while read LINE
do
    if [[ $LINE =~ ${regex[comment]} ]]
    then
        printf "Comment: %s\n" "${BASH_REMATCH[1]}"
        continue
    elif [[ $LINE =~ ${regex[ppa]} ]]
    then
        ppa=${BASH_REMATCH[1]}
        printf "PPA: %s\n" "$ppa"
    elif [[ $LINE =~ ${regex[apt]} ]]
    then
        package=${BASH_REMATCH[1]}
        printf "APT: %s\n" "$package"
    elif [[ $LINE =~ ${regex[deb]} ]]
    then
        package_name=${BASH_REMATCH[1]}
        url=${BASH_REMATCH[2]}
        target_path=${BASH_REMATCH[3]}
        file_name=${BASH_REMATCH[4]}
        file_path="$target_path/$file_name"

        printf "package_name: %s\n" "$package_name"
        printf "url: %s\n" "$url"
        printf "file_path: %s\n" "$file_path"
    fi
done < "$1"

With your example file:

$ ./foo.sh foo.txt
Comment:  ppa
PPA: deadsnakes/ppa
Comment:  packages
APT: tree
Comment:  deb
package_name: fzy
url: https://github.com/jhawthorn/fzy/releases/download/0.9/fzy_0.9-1_amd64.deb
file_path: $HOME/fzy_0.9-1_amd64.deb
3

There's already a format for controlling apt using a file. It's an ordinary shell script.

#!/bin/sh

# Instructions: Run me using sudo: $ sudo install_script.sh
# WARNING: All errors have been muted or thrown out. Guess I don't want to know...

# PPA
add-apt-repository -y ppa:deadsnakes/ppa &> /dev/null
add-apt-repository -y ppa:font-manager/staging &> /dev/null
add-apt-repository -y ppa:fish-shell/release-2 &> /dev/null
add-apt-repository -y ppa:zanchey/asciinema &> /dev/null

# packages
apt install --allow-unauthenticated -qqy tree symlinks

# deb
wget https://github.com/jhawthorn/fzy/releases/download/0.9/fzy_0.9-1_amd64.deb -O /tmp/fzy_0.9-1_amd64.deb
apt --install /tmp/fzy_0.9-1_amd64.deb -qqy
rm /tmp/fzy_0.9-1_amd64.deb

There are lots of ways to make a script more complex and fun to play with.

There are also lots of ways to clean up a script and make it more readable and maintainable for you.

  • 1
    Thank you for your answer. I appreciate it. However, this does not answer my question. – Nicholas Adamou Jul 8 '18 at 14:18
  • 2
    Future readers will benefit from knowing the standard, as well as your variation upon that standard. – user535733 Jul 8 '18 at 14:21
  • I like it. Totally to the point so it looks clean and is easy to understand. I spent years making my own scripts in the past and this would have saved me a lot of time ;) so +1 :) – Rinzwind Jul 8 '18 at 16:36
1

Background:

After a bit of research and trial and error, I figured out I could use the tool cut in conjunction with echo.

Example:

To achieve the desired output take the following example.

packages:

# packages
apt "tree"

test.sh:

apt_install_from_file() {

    declare -r FILE_PATH="$1"

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Install package(s)

    if [ -e "$FILE_PATH" ]; then

        cat < "$FILE_PATH" | while read -r LINE; do

            if [[ "$LINE" == *"#"* || -z "$LINE" ]]; then
                continue
            elif [[ "$LINE" == *"apt"* || -z "$LINE" ]]; then
                package="$(echo $LINE | cut -d \" -f2)"
                echo "$package"
            fi

        done
    fi

}

Usage:

./test.sh packages

Breakdown:

Given the line package="$(echo $LINE | cut -d \" -f2)", lets break it down.

  1. First, we echo the contents of the line.
  2. Then, we pip that result to cut which splits the contents of $LINE into multiple fields based on a given delimiter. The delimiter in our case is ". We had to place a \ in front of the " because the quote character must be escaped.
  3. Next, we then grab the second field of that split, which contains tree.
  4. Finally, we store the contents of what $() returns into package of which case is tree (e.g. $() => tree).

Modified Function:

apt_install_from_file() {

    declare -r FILE_PATH="$1"

    # - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -

    # Install package(s)

    if [ -e "$FILE_PATH" ]; then

        cat < "$FILE_PATH" | while read -r LINE; do
            if [[ "$LINE" == *"#"* || -z "$LINE" ]]; then
                continue
            elif [[ "$LINE" == *"ppa"* || -z "$LINE" ]]; then
                ppa="$(echo $LINE | cut -d \" -f2)"
                sudo add-apt-repository -y ppa:"$ppa" &> /dev/null \
                   && sudo apt update
            elif [[ "$LINE" == *"apt"* || -z "$LINE" ]]; then
                package="$(echo $LINE | cut -d \" -f2)"
                sudo apt install --allow-unauthenticated -qqy "$package"
            elif [[ "$LINE" == *"deb"* || -z "$LINE" ]]; then
                package_name="$(echo $LINE | cut -d \" -f2)"
                url="$(echo $LINE | cut -d \" -f4)"
                target_path="$(echo $LINE | cut -d \" -f6)"
                file_name="$(echo $LINE | cut -d \" -f8)"
                file_path="$target_path/$file_name"  

                wget $url -O $file_path && \
                   sudo dpkg -i $file_path && sudo apt-get install -f && \
                   sudo rm -rf $file_path && sudo apt autoremove -qqy
            fi
        done

    fi

}

Conclusion:

This may not be the best solution to my problem I listed above and that it is indeed a hacky solution, so if there is a better one, please do not hesitate to write an updated answer!

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.